package com.ikingtech.platform.service.system.dept.controller;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ikingtech.framework.sdk.base.model.DragOrderParam;
import com.ikingtech.framework.sdk.base.model.PageResult;
import com.ikingtech.framework.sdk.context.event.TenantDeleteEvent;
import com.ikingtech.framework.sdk.context.event.TenantInitEvent;
import com.ikingtech.framework.sdk.context.exception.FrameworkException;
import com.ikingtech.framework.sdk.context.security.Me;
import com.ikingtech.framework.sdk.core.response.R;
import com.ikingtech.framework.sdk.data.mybatisplus.helper.DragHelper;
import com.ikingtech.framework.sdk.data.mybatisplus.helper.DragHelperBuilder;
import com.ikingtech.framework.sdk.department.api.DeptApi;
import com.ikingtech.framework.sdk.department.api.DeptDictApi;
import com.ikingtech.framework.sdk.department.api.DeptRoleDataScopeApi;
import com.ikingtech.framework.sdk.department.api.DeptUserApi;
import com.ikingtech.framework.sdk.department.model.DeptBasicDTO;
import com.ikingtech.framework.sdk.department.model.DeptDTO;
import com.ikingtech.framework.sdk.department.model.DeptQueryParamDTO;
import com.ikingtech.framework.sdk.enums.common.DragTargetPositionEnum;
import com.ikingtech.framework.sdk.enums.system.department.DeptTypeEnum;
import com.ikingtech.framework.sdk.log.embedded.annotation.OperationLog;
import com.ikingtech.framework.sdk.utils.Tools;
import com.ikingtech.framework.sdk.web.annotation.ApiController;
import com.ikingtech.platform.service.system.dept.entity.DepartmentDO;
import com.ikingtech.platform.service.system.dept.exception.DeptExceptionInfo;
import com.ikingtech.platform.service.system.dept.service.repository.DeptRepository;
import com.ikingtech.platform.service.system.dept.service.repository.ModelConverter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author tie yan
 */
@Slf4j
@RequiredArgsConstructor
@ApiController(value = "/system/dept", name = "系统管理-部门管理", description = "系统管理-部门管理，不支持多个根部门，所有部门均归属于默认根部门，默认根部门编号为root")
public class DeptController implements DeptApi {

    private final DeptRepository repo;

    private final DeptUserApi deptUserApi;

    private final DeptDictApi deptDictApi;

    private final DeptRoleDataScopeApi deptRoleDataScopeApi;

    private final ModelConverter converter;

    @Override
    @OperationLog(value = "新增部门", dataId = "#_res.getData()")
    @Transactional(rollbackFor = Exception.class)
    public R<String> add(DeptDTO dept) {
        if (this.repo.exists(Wrappers.<DepartmentDO>lambdaQuery()
                .and(Tools.Str.isBlank(dept.getParentId()), wrapper -> wrapper.eq(DepartmentDO::getParentId, Tools.Str.EMPTY).or().isNull(DepartmentDO::getParentId))
                .eq(Tools.Str.isNotBlank(dept.getParentId()), DepartmentDO::getParentId, dept.getParentId())
                .eq(DepartmentDO::getName, dept.getName())
                .eq(Tools.Str.isNotBlank(Me.tenantCode()), DepartmentDO::getTenantCode, Me.tenantCode()))) {
            throw new FrameworkException(DeptExceptionInfo.DUPLICATE_DEPT_NAME);
        }
        DepartmentDO entity = Tools.Bean.copy(dept, DepartmentDO.class);
        entity.setId(Tools.Id.uuid());
        entity.setTenantCode(Me.tenantCode());
        if (null != dept.getCategoryInfo()) {
            entity.setCategory(dept.getCategoryInfo().value());
        }
        DepartmentDO parentEntity = this.repo.getById(entity.getParentId());
        entity.setFullPath(this.parseFullPath(parentEntity, entity.getId()));
        if (DeptTypeEnum.UNITY.equals(dept.getType())) {
            entity.setGrade(null == parentEntity ? 1 : parentEntity.getGrade() + 1);
        } else {
            entity.setGrade(1);
        }
        entity.setSortOrder(this.getMaxSortOrder(entity.getParentId(), Me.tenantCode()) + 1);
        this.repo.save(entity);
        return R.ok(entity.getId());
    }

    @Override
    @OperationLog(value = "删除部门")
    @Transactional(rollbackFor = Exception.class)
    public R<Object> delete(String id) {
        DepartmentDO entity = this.repo.getById(id);
        if (null == entity) {
            return R.ok();
        }
        List<String> ids = this.repo.listObjs(Wrappers.<DepartmentDO>lambdaQuery().select(DepartmentDO::getId).likeRight(DepartmentDO::getFullPath, entity.getFullPath()));
        this.deptUserApi.moveUser(entity.getParentId(), ids, Me.tenantCode());
        this.deptRoleDataScopeApi.removeRoleDataScopeByDeptIds(ids, Me.tenantCode());
        this.repo.removeBatchByIds(ids);
        return R.ok();
    }

    @Override
    @OperationLog(value = "更新部门")
    @Transactional(rollbackFor = Exception.class)
    public R<Object> update(DeptDTO dept) {
        if (!this.repo.exists(Wrappers.<DepartmentDO>lambdaQuery().eq(DepartmentDO::getId, dept.getId()))) {
            throw new FrameworkException(DeptExceptionInfo.DEPT_NOT_FOUND);
        }
        if (this.repo.exists(Wrappers.<DepartmentDO>lambdaQuery()
                .ne(DepartmentDO::getId, dept.getId())
                .and(Tools.Str.isBlank(dept.getParentId()), wrapper -> wrapper.eq(DepartmentDO::getParentId, Tools.Str.EMPTY).or().isNull(DepartmentDO::getParentId))
                .eq(Tools.Str.isNotBlank(dept.getParentId()), DepartmentDO::getParentId, dept.getParentId())
                .eq(DepartmentDO::getName, dept.getName())
                .eq(Tools.Str.isNotBlank(Me.tenantCode()), DepartmentDO::getTenantCode, Me.tenantCode()))) {
            throw new FrameworkException(DeptExceptionInfo.DUPLICATE_DEPT_NAME);
        }
        DepartmentDO entity = Tools.Bean.copy(dept, DepartmentDO.class);
        if (null != dept.getCategoryInfo()) {
            entity.setCategory(dept.getCategoryInfo().value());
        }
        entity.setTenantCode(Me.tenantCode());
        entity.setFullPath(this.parseFullPath(this.repo.getById(entity.getParentId()), entity.getId()));
        this.repo.updateById(entity);
        return R.ok();
    }

    @Override
    public R<List<DeptDTO>> page(DeptQueryParamDTO queryParam) {
        List<String> dataScope = Me.dataScope();
        if (Me.invalidDataScope(dataScope)) {
            return R.ok(new ArrayList<>());
        }
        queryParam.setIds(dataScope);
        return R.ok(PageResult.build(this.repo.page(new Page<>(queryParam.getPage(), queryParam.getRows()),
                        DeptRepository.createQueryWrapper(queryParam)))
                .convertBatch(entities -> this.converter.modelConvert(entities, dataScope, true)));
    }

    @Override
    public R<List<DeptBasicDTO>> all(String excludeId) {
        List<String> dataScopeCodes = Me.dataScope();
        if (Me.invalidDataScope(dataScopeCodes)) {
            return R.ok(new ArrayList<>());
        }
        List<String> excludeDeptIds = new ArrayList<>();
        if (Tools.Str.isNotBlank(excludeId)) {
            DepartmentDO entity = this.repo.getById(excludeId);
            if (null != entity) {
                excludeDeptIds.addAll(this.repo.listObjs(Wrappers.<DepartmentDO>lambdaQuery().select(DepartmentDO::getId).likeRight(DepartmentDO::getFullPath, entity.getFullPath())));
            }
        }
        List<DepartmentDO> entities = this.repo.list(Wrappers.<DepartmentDO>lambdaQuery()
                .in(Tools.Coll.isNotBlank(dataScopeCodes), DepartmentDO::getId, dataScopeCodes)
                .notIn(Tools.Coll.isNotBlank(excludeDeptIds), DepartmentDO::getId, excludeDeptIds)
                .eq(DepartmentDO::getTenantCode, Me.tenantCode())
                .orderByDesc(DepartmentDO::getCreateTime));
        this.retrieveQueriedEntities(entities);
        return R.ok(this.converter.modelInfoConvert(entities));
    }

    @Override
    public R<List<DeptBasicDTO>> list(DeptQueryParamDTO queryParam) {
        return R.ok(this.converter.modelInfoConvert(this.repo.list(DeptRepository.createQueryWrapper(queryParam, Me.tenantCode()))));
    }

    @Override
    public R<DeptDTO> detail(String id) {
        DepartmentDO entity = this.repo.getById(id);
        if (null == entity) {
            throw new FrameworkException(DeptExceptionInfo.DEPT_NOT_FOUND);
        }
        List<String> subEntityIds = this.repo.listObjs(Wrappers.<DepartmentDO>lambdaQuery()
                .select(DepartmentDO::getId)
                .likeRight(DepartmentDO::getFullPath, entity.getFullPath()));
        return R.ok(this.converter.modelConvert(entity,
                this.repo.findUnity(entity),
                this.deptUserApi.loadManager(entity.getManagerId()),
                this.deptUserApi.getUserCount(subEntityIds),
                this.deptDictApi.load(entity.getCategory(), Me.tenantCode())));
    }

    @Override
    public R<List<DeptDTO>> listSubAll(DeptQueryParamDTO queryParam) {
        List<DepartmentDO> entities = this.listSub(queryParam.getParentDeptId());
        if (Tools.Coll.isBlank(entities)) {
            return R.ok(Collections.emptyList());
        }
        this.retrieveQueriedEntities(entities);
        return R.ok(this.converter.modelConvert(entities, Me.dataScope(), true));
    }

    @Override
    public R<List<DeptBasicDTO>> listSubInfoAll(DeptQueryParamDTO queryParam) {
        List<DepartmentDO> entities = this.listSub(queryParam.getParentDeptId());
        if (Tools.Coll.isBlank(entities)) {
            return R.ok(Collections.emptyList());
        }
        this.retrieveQueriedEntities(entities);
        return R.ok(this.converter.modelInfoConvert(entities));
    }

    private List<DepartmentDO> listSub(String parentDeptId) {
        List<String> dataScope = Me.dataScope();
        if (Me.invalidDataScope(dataScope)) {
            return Collections.emptyList();
        }
        if (Tools.Str.isNotBlank(parentDeptId)) {
            String fullPath = this.repo.getObj(Wrappers.<DepartmentDO>lambdaQuery()
                    .select(DepartmentDO::getFullPath)
                    .eq(DepartmentDO::getId, parentDeptId), String.class::cast);
            if (Tools.Str.isBlank(fullPath)) {
                return Collections.emptyList();
            }
            dataScope = this.repo.listObjs(Wrappers.<DepartmentDO>lambdaQuery()
                    .select(DepartmentDO::getId)
                    .in(Tools.Coll.isNotBlank(dataScope), DepartmentDO::getId, dataScope)
                    .likeRight(DepartmentDO::getFullPath, fullPath));
            if (Tools.Coll.isBlank(dataScope)) {
                return Collections.emptyList();
            }
        }
        return this.repo.list(Wrappers.<DepartmentDO>lambdaQuery()
                .in(Tools.Coll.isNotBlank(dataScope), DepartmentDO::getId, dataScope));
    }

    private void retrieveQueriedEntities(List<DepartmentDO> entities) {
        if (entities.size() < 2) {
            return;
        }
        List<String> queriedDeptIds = Tools.Coll.convertList(entities, DepartmentDO::getId);
        List<DepartmentDO> sortedEntities = Tools.Coll.sort(entities, entity -> entity.getFullPath().length());
        List<String> additionalDeptIds = new ArrayList<>();
        for (int i = 0; i < sortedEntities.size(); i++) {
            List<String> fullPathDeptIds = Tools.Str.split(sortedEntities.get(i).getFullPath(), "@");
            for (int j = fullPathDeptIds.size() - 1; j >= 0; j--) {
                boolean isCommon = false;
                for (DepartmentDO sortedEntity : sortedEntities) {
                    additionalDeptIds.add(fullPathDeptIds.get(j));
                    if (sortedEntity.getFullPath().contains(fullPathDeptIds.get(j))) {
                        isCommon = true;
                        break;
                    }
                }
                if (isCommon) {
                    break;
                }
            }
        }
        if (Tools.Coll.isNotBlank(Tools.Coll.filter(additionalDeptIds, additionalDeptId -> !queriedDeptIds.contains(additionalDeptId)))) {
            entities.addAll(this.repo.listByIds(additionalDeptIds));
        }
    }

    @Override
    public R<Object> drag(DragOrderParam dragParam) {
        if (DragTargetPositionEnum.NONE.equals(dragParam.getPosition())) {
            return R.ok();
        }
        DragHelper<DepartmentDO> dragHelper = DragHelperBuilder.builder(
                        () -> this.repo.list(Wrappers.<DepartmentDO>query().lambda()
                                .eq(DepartmentDO::getParentId, dragParam.getParentId())
                                .gt(DepartmentDO::getSortOrder, dragParam.getCurrentOrder())
                                .orderByAsc(DepartmentDO::getSortOrder)
                        ),
                        () -> this.repo.list(Wrappers.<DepartmentDO>query().lambda()
                                .eq(DepartmentDO::getParentId, dragParam.getTargetParentId())
                                .gt(DepartmentDO::getSortOrder, dragParam.getTargetOrder())
                                .orderByAsc(DepartmentDO::getSortOrder)
                        ),
                        (starOrder, endOrder) -> this.repo.list(Wrappers.<DepartmentDO>query().lambda()
                                .eq(DepartmentDO::getParentId, dragParam.getParentId())
                                .between(DepartmentDO::getSortOrder, starOrder, endOrder)
                        )
                )
                .which(dragParam.getParentId(), dragParam.getTargetParentId(), DragTargetPositionEnum.INNER.equals(dragParam.getPosition()))
                .currentNode(() -> this.repo.getById(dragParam.getCurrentId()))
                .targetNode(() -> this.repo.getById(dragParam.getTargetId()))
                .maxSortOrder(() -> this.getMaxSortOrder(dragParam.getTargetParentId(), Me.tenantCode()))
                .beforeTarget(DragTargetPositionEnum.BEFORE.equals(dragParam.getPosition()))
                .build();
        this.repo.updateBatchById(dragHelper.drag());
        return R.ok();
    }

    @EventListener
    public void tenantInitEventListener(TenantInitEvent event) {
        DepartmentDO entity = new DepartmentDO();
        entity.setId(Tools.Id.uuid());
        entity.setName(event.getName());
        entity.setParentId(Tools.Str.EMPTY);
        entity.setFullPath(entity.getId());
        entity.setTenantCode(event.getCode());
        entity.setSortOrder(this.getMaxSortOrder(entity.getParentId(), Me.tenantCode()) + 1);
        this.repo.save(entity);
    }

    @EventListener
    public void tenantDeleteEventListener(TenantDeleteEvent event) {
        this.repo.remove(Wrappers.<DepartmentDO>lambdaQuery().eq(DepartmentDO::getTenantCode, event.getCode()));
    }

    private String parseFullPath(DepartmentDO parentEntity, String id) {
        if (null != parentEntity) {
            if (Tools.Str.isBlank(parentEntity.getFullPath())) {
                return id;
            }
            return parentEntity.getFullPath() + "@" + id;
        } else {
            return id;
        }
    }

    private Integer getMaxSortOrder(String parentId, String tenantCode) {
        List<Number> orders = this.repo.listObjs(Wrappers.<DepartmentDO>lambdaQuery()
                .select(DepartmentDO::getSortOrder)
                .eq(DepartmentDO::getParentId, Tools.Str.isBlank(parentId) ? Tools.Str.EMPTY : parentId)
                .eq(Tools.Str.isNotBlank(tenantCode), DepartmentDO::getTenantCode, tenantCode)
                .orderByDesc(DepartmentDO::getSortOrder));
        return Tools.Coll.isBlank(orders) ? 0 : orders.get(0).intValue();
    }
}
